Inverview Scheduler

工作流概述

这是一个包含25个节点的复杂工作流,主要用于自动化处理各种任务。

工作流源代码

下载
{
  "id": "bh3H2b654RSYgIm9",
  "meta": {
    "instanceId": "efb474b59b0341d7791932605bd9ff04a6c7ed9941fdd53dc4a2e4b99a6f9439",
    "templateCredsSetupCompleted": true
  },
  "name": "Inverview Scheduler",
  "tags": [],
  "nodes": [
    {
      "id": "cd5664f9-0b6b-491a-a0a0-1d8b3b2f2461",
      "name": "OpenAI Chat Model2",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        320,
        1480
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "id": "ghJTvay8CvwXDsXz",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e8ca4a14-ee58-4be0-838b-5cbf8a802b6e",
      "name": "Window Buffer Memory2",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        520,
        1480
      ],
      "parameters": {
        "sessionKey": "={{ $json.sessionId }}",
        "sessionIdType": "customKey",
        "contextWindowLength": 10
      },
      "typeVersion": 1.3
    },
    {
      "id": "d2957530-acd1-4875-a75b-69b890f08065",
      "name": "OpenAI Chat Model4",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1220,
        1440
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "id": "ghJTvay8CvwXDsXz",
          "name": "OpenAi account"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "897c8189-aaa9-45c7-99c6-95378a7a13f2",
      "name": "Run Get Availability",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        720,
        1520
      ],
      "parameters": {
        "name": "get_availability",
        "source": "parameter",
        "description": "Call this tool to get my availability",
        "workflowJson": "{
  \"nodes\": [
    {
      \"parameters\": {
        \"operation\": \"getAll\",
        \"calendar\": {
          \"__rl\": true,
          \"value\": \"rbreen.ynteractive@gmail.com\",
          \"mode\": \"list\",
          \"cachedResultName\": \"rbreen.ynteractive@gmail.com\"
        },
        \"returnAll\": true,
        \"options\": {
          \"fields\": \"\"
        }
      },
      \"type\": \"n8n-nodes-base.googleCalendar\",
      \"typeVersion\": 1.3,
      \"position\": [
        -500,
        220
      ],
      \"id\": \"a1017705-8866-469f-83e0-9f5d5f37af53\",
      \"name\": \"Check My Calendar\",
      \"credentials\": {
        \"googleCalendarOAuth2Api\": {
          \"id\": \"nc5M45R7LyFadByw\",
          \"name\": \"Google Calendar account\"
        }
      }
    },
    {
      \"parameters\": {
        \"jsCode\": \"const events = items.map(item => item.json);\nconst intervalMinutes = 30;\nconst timeZone = 'America/New_York';\n\nfunction formatToEastern(date) {\n  const tzDate = new Intl.DateTimeFormat('en-US', {\n    timeZone,\n    year: 'numeric',\n    month: '2-digit',\n    day: '2-digit',\n    hour: '2-digit',\n    minute: '2-digit',\n    second: '2-digit',\n    hour12: false\n  }).formatToParts(date).reduce((acc, part) => {\n    if (part.type !== 'literal') acc[part.type] = part.value;\n    return acc;\n  }, {});\n\n  const offset = getEasternOffset(date);\n  return `${tzDate.year}-${tzDate.month}-${tzDate.day}T${tzDate.hour}:${tzDate.minute}:${tzDate.second}${offset}`;\n}\n\nfunction getEasternOffset(date) {\n  const options = { timeZone, timeZoneName: 'short' };\n  const parts = new Intl.DateTimeFormat('en-US', options).formatToParts(date);\n  const tzName = parts.find(p => p.type === 'timeZoneName').value;\n  return tzName.includes('EDT') ? '-04:00' : '-05:00';\n}\n\nfunction alignToPreviousSlot(date) {\n  const aligned = new Date(date);\n  const minutes = aligned.getMinutes();\n  aligned.setMinutes(minutes < 30 ? 0 : 30, 0, 0);\n  return aligned;\n}\n\nfunction alignToNextSlot(date) {\n  const aligned = new Date(date);\n  const minutes = aligned.getMinutes();\n  if (minutes > 0 && minutes <= 30) {\n    aligned.setMinutes(30, 0, 0);\n  } else if (minutes > 30) {\n    aligned.setHours(aligned.getHours() + 1);\n    aligned.setMinutes(0, 0, 0);\n  } else {\n    aligned.setMinutes(0, 0, 0);\n  }\n  return aligned;\n}\n\nconst splitEventIntoETBlocks = (event) => {\n  const blocks = [];\n\n  let current = alignToPreviousSlot(new Date(event.start.dateTime));\n  const eventEnd = alignToNextSlot(new Date(event.end.dateTime));\n\n  while (current < eventEnd) {\n    const blockEnd = new Date(current);\n    blockEnd.setMinutes(current.getMinutes() + intervalMinutes);\n\n    blocks.push({\n      start: formatToEastern(current),\n      end: formatToEastern(blockEnd)\n    });\n\n    current = blockEnd;\n  }\n\n  return blocks;\n};\n\nlet allBlocks = [];\nfor (const event of events) {\n  if (event.start?.dateTime && event.end?.dateTime) {\n    const blocks = splitEventIntoETBlocks(event);\n    allBlocks = allBlocks.concat(blocks);\n  }\n}\n\nreturn allBlocks.map(block => ({ json: block }));\n\"
      },
      \"type\": \"n8n-nodes-base.code\",
      \"typeVersion\": 2,
      \"position\": [
        -280,
        240
      ],
      \"id\": \"fb9063c2-de6b-4513-8901-d12625f5d772\",
      \"name\": \"Split Events into 30 min blocks\"
    },
    {
      \"parameters\": {
        \"assignments\": {
          \"assignments\": [
            {
              \"id\": \"f1270be8-1d11-4086-8bc0-ae53c99507c1\",
              \"name\": \"start\",
              \"value\": \"={{ $json.start }}\",
              \"type\": \"string\"
            },
            {
              \"id\": \"1a5f24ff-7d0c-436d-bb0b-015fc0c85cb7\",
              \"name\": \"end\",
              \"value\": \"={{ $json.end }}\",
              \"type\": \"string\"
            },
            {
              \"id\": \"befe6645-c0c1-40eb-9ba6-eccf2a762247\",
              \"name\": \"Blocked\",
              \"value\": \"Blocked\",
              \"type\": \"string\"
            }
          ]
        },
        \"options\": {}
      },
      \"type\": \"n8n-nodes-base.set\",
      \"typeVersion\": 3.4,
      \"position\": [
        -80,
        240
      ],
      \"id\": \"23d8ed50-131f-49ea-9ce8-72a0067fe828\",
      \"name\": \"Add Blocked Field\"
    },
    {
      \"parameters\": {
        \"jsCode\": \"const slots = [];\nconst slotMinutes = 30;\nconst timeZone = 'America/New_York';\nconst businessStartHour = 9;\nconst businessEndHour = 17;\n\n// Get offset like -04:00 or -05:00\nfunction getEasternOffset(date) {\n  const options = { timeZone, timeZoneName: 'short' };\n  const parts = new Intl.DateTimeFormat('en-US', options).formatToParts(date);\n  const tz = parts.find(p => p.type === 'timeZoneName')?.value || 'EST';\n  return tz.includes('EDT') ? '-04:00' : '-05:00';\n}\n\n// Format Date as ISO with Eastern offset\nfunction formatToEasternISO(date) {\n  const formatter = new Intl.DateTimeFormat('en-CA', {\n    timeZone,\n    year: 'numeric',\n    month: '2-digit',\n    day: '2-digit',\n    hour: '2-digit',\n    minute: '2-digit',\n    second: '2-digit',\n    hour12: false,\n  });\n\n  const parts = formatter.formatToParts(date).reduce((acc, part) => {\n    if (part.type !== 'literal') acc[part.type] = part.value;\n    return acc;\n  }, {});\n\n  const offset = getEasternOffset(date);\n  return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}${offset}`;\n}\n\n// Convert a Date to the hour/minute of its Eastern time\nfunction getEasternTimeParts(date) {\n  const formatter = new Intl.DateTimeFormat('en-US', {\n    timeZone,\n    hour: '2-digit',\n    minute: '2-digit',\n    hour12: false,\n  });\n  const [hourStr, minStr] = formatter.format(date).split(':');\n  return { hour: parseInt(hourStr), minute: parseInt(minStr) };\n}\n\nconst now = new Date();\nconst endDate = new Date(now);\nendDate.setDate(now.getDate() + 7);\n\n// Set current time to 24 hours in the future\nconst current = new Date(now);\ncurrent.setHours(current.getHours() + 24);\n\n// Round to the next 30-minute block in Eastern time\nconst { minute } = getEasternTimeParts(current);\nif (minute < 30) {\n  current.setMinutes(30, 0, 0);\n} else {\n  current.setHours(current.getHours() + 1);\n  current.setMinutes(0, 0, 0);\n}\n\n// Generate 30-minute blocks only during business hours & weekdays\nwhile (current < endDate) {\n  const dayOfWeek = current.getDay(); // 0 = Sunday, 6 = Saturday\n\n  // Skip weekends\n  if (dayOfWeek !== 0 && dayOfWeek !== 6) {\n    const { hour } = getEasternTimeParts(current);\n\n    if (hour >= businessStartHour && hour < businessEndHour) {\n      const start = new Date(current);\n      const end = new Date(start);\n      end.setMinutes(start.getMinutes() + slotMinutes);\n\n      slots.push({\n        start: formatToEasternISO(start),\n        end: formatToEasternISO(end),\n      });\n    }\n  }\n\n  current.setMinutes(current.getMinutes() + slotMinutes);\n}\n\nreturn slots.map(slot => ({ json: slot }));\n\"
      },
      \"type\": \"n8n-nodes-base.code\",
      \"typeVersion\": 2,
      \"position\": [
        -400,
        460
      ],
      \"id\": \"01597a94-d94b-47e7-9488-adea3abb741c\",
      \"name\": \"Generate 30 Minute Timeslots\"
    },
    {
      \"parameters\": {
        \"mode\": \"combine\",
        \"fieldsToMatchString\": \"start, end\",
        \"joinMode\": \"enrichInput2\",
        \"options\": {}
      },
      \"type\": \"n8n-nodes-base.merge\",
      \"typeVersion\": 3,
      \"position\": [
        180,
        300
      ],
      \"id\": \"2d9f98a1-02ac-4332-a288-635a48ea3ee8\",
      \"name\": \"Combine My Calendar with All Slots\"
    },
    {
      \"parameters\": {
        \"conditions\": {
          \"options\": {
            \"caseSensitive\": true,
            \"leftValue\": \"\",
            \"typeValidation\": \"strict\",
            \"version\": 2
          },
          \"conditions\": [
            {
              \"id\": \"af65c6c8-31c7-4f27-a073-cf7f72079882\",
              \"leftValue\": \"={{ $json.Blocked }}\",
              \"rightValue\": \"Blocked\",
              \"operator\": {
                \"type\": \"string\",
                \"operation\": \"notEquals\"
              }
            }
          ],
          \"combinator\": \"and\"
        },
        \"options\": {}
      },
      \"type\": \"n8n-nodes-base.if\",
      \"typeVersion\": 2.2,
      \"position\": [
        420,
        280
      ],
      \"id\": \"0438b5be-b3c4-4645-9604-303ace7bfead\",
      \"name\": \"Check if Calendar Blocked\"
    },
    {
      \"parameters\": {
        \"jsCode\": \"const formatted = items.map(item => {\n  const start = item.json.start;\n  const end = item.json.end;\n  return `${start} - ${end}`;\n});\n\nconst combined = formatted.join(', ');\n\nreturn [\n  {\n    json: {\n      availableSlots: combined\n    }\n  }\n];\n\"
      },
      \"type\": \"n8n-nodes-base.code\",
      \"typeVersion\": 2,
      \"position\": [
        660,
        300
      ],
      \"id\": \"4a6bfde4-7d9f-4837-bc6c-66bf968e782a\",
      \"name\": \"Return string of all available times\"
    },
    {
      \"parameters\": {
        \"inputSource\": \"passthrough\"
      },
      \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",
      \"typeVersion\": 1.1,
      \"position\": [
        -760,
        340
      ],
      \"id\": \"8bde95cb-7239-4b7d-aca1-0adacf2ea257\",
      \"name\": \"Get Availability\"
    }
  ],
  \"connections\": {
    \"Check My Calendar\": {
      \"main\": [
        [
          {
            \"node\": \"Split Events into 30 min blocks\",
            \"type\": \"main\",
            \"index\": 0
          }
        ]
      ]
    },
    \"Split Events into 30 min blocks\": {
      \"main\": [
        [
          {
            \"node\": \"Add Blocked Field\",
            \"type\": \"main\",
            \"index\": 0
          }
        ]
      ]
    },
    \"Add Blocked Field\": {
      \"main\": [
        [
          {
            \"node\": \"Combine My Calendar with All Slots\",
            \"type\": \"main\",
            \"index\": 0
          }
        ]
      ]
    },
    \"Generate 30 Minute Timeslots\": {
      \"main\": [
        [
          {
            \"node\": \"Combine My Calendar with All Slots\",
            \"type\": \"main\",
            \"index\": 1
          }
        ]
      ]
    },
    \"Combine My Calendar with All Slots\": {
      \"main\": [
        [
          {
            \"node\": \"Check if Calendar Blocked\",
            \"type\": \"main\",
            \"index\": 0
          }
        ]
      ]
    },
    \"Check if Calendar Blocked\": {
      \"main\": [
        [
          {
            \"node\": \"Return string of all available times\",
            \"type\": \"main\",
            \"index\": 0
          }
        ]
      ]
    },
    \"Get Availability\": {
      \"main\": [
        [
          {
            \"node\": \"Check My Calendar\",
            \"type\": \"main\",
            \"index\": 0
          },
          {
            \"node\": \"Generate 30 Minute Timeslots\",
            \"type\": \"main\",
            \"index\": 0
          }
        ]
      ]
    }
  },
  \"pinData\": {},
  \"meta\": {
    \"instanceId\": \"efb474b59b0341d7791932605bd9ff04a6c7ed9941fdd53dc4a2e4b99a6f9439\"
  }
}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "8892f883-aaae-4616-bb50-bbe0f9dacb23",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1440,
        1660
      ],
      "parameters": {
        "color": 3,
        "width": 520,
        "height": 480,
        "content": "Check Day Names Tool


1. This part of the flow is just a copy of what is embedded in the \"Check Day Names Tool\". It does not run. 

2. If you update this part of the flow, copy it with ctrl-c and paste it into another workbook. Add a sub-workflow execution. Set the workflow to accept all data. Copy the flow. Paste the Workflow JSON field in the \"Check Day Names Tool\" tool node
"
      },
      "typeVersion": 1
    },
    {
      "id": "234b89da-9003-43d5-842a-4ecf92522b51",
      "name": "check day names",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        880,
        1480
      ],
      "parameters": {
        "name": "check_days",
        "source": "parameter",
        "workflowJson": "{
  \"nodes\": [
    {
      \"parameters\": {
        \"inputSource\": \"passthrough\"
      },
      \"type\": \"n8n-nodes-base.executeWorkflowTrigger\",
      \"typeVersion\": 1.1,
      \"position\": [
        -400,
        -120
      ],
      \"id\": \"dec37e15-3695-4911-91a6-1f97018ab982\",
      \"name\": \"When Executed by Another Workflow\"
    },
    {
      \"parameters\": {
        \"jsCode\": \"function getWeekdaysNextTwoWeeks() {\n  const items = [];\n  const longDayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];\n\n  const today = new Date();\n  const endDate = new Date();\n  endDate.setDate(today.getDate() + 14); // 2 weeks ahead\n\n  const current = new Date(today);\n\n  while (current <= endDate) {\n    const dayOfWeek = current.getDay(); // 0 = Sunday, 6 = Saturday\n\n    // Only weekdays (Mon–Fri)\n    if (dayOfWeek >= 1 && dayOfWeek <= 5) {\n      const dateStr = current.toISOString().split('T')[0]; // YYYY-MM-DD\n      const output = `${longDayNames[dayOfWeek]} - ${dateStr}`;\n\n      items.push({\n        json: {\n          day: output\n        }\n      });\n    }\n\n    current.setDate(current.getDate() + 1); // Go to next day\n  }\n\n  return items;\n}\n\n// Example usage:\nreturn getWeekdaysNextTwoWeeks();\n\"
      },
      \"type\": \"n8n-nodes-base.code\",
      \"typeVersion\": 2,
      \"position\": [
        -180,
        -120
      ],
      \"id\": \"cbbe4248-d1cc-48e3-9ea8-67a844f3de29\",
      \"name\": \"Check Day Names\"
    }
  ],
  \"connections\": {
    \"When Executed by Another Workflow\": {
      \"main\": [
        [
          {
            \"node\": \"Check Day Names\",
            \"type\": \"main\",
            \"index\": 0
          }
        ]
      ]
    }
  },
  \"pinData\": {},
  \"meta\": {
    \"instanceId\": \"efb474b59b0341d7791932605bd9ff04a6c7ed9941fdd53dc4a2e4b99a6f9439\"
  }
}"
      },
      "typeVersion": 2.1
    },
    {
      "id": "c052c7e4-1587-4c7e-9a8e-043c8571338d",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        180,
        1660
      ],
      "parameters": {
        "width": 1200,
        "height": 500,
        "content": "Get Availability Execution. 

1. This part of the flow is just a copy of what is embedded in the \"Run Get Availability Tool\". It does not run. 

2. If you update this part of the flow, copy it with ctrl-c and paste it into another workbook. Add a sub-workflow execution. Set the workflow to accept all data. Copy the flow. Paste the Workflow JSON field in the \"Run Get Availability\" tool node"
      },
      "typeVersion": 1
    },
    {
      "id": "b7c71153-fbd1-45ac-8dbf-d4beb241daaf",
      "name": "Convert Output to JSON",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1240,
        1260
      ],
      "parameters": {
        "text": "={{ $json.output }}",
        "options": {
          "systemMessage": "=take in this message and output json"
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.7
    },
    {
      "id": "1f902158-5885-46d6-8d7e-26ccf116ed0a",
      "name": "Interview Scheduler",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        520,
        1220
      ],
      "parameters": {
        "text": "={{ $json.chatInput }}",
        "options": {
          "systemMessage": "=You are a friendly AI chatbot helping users schedule meetings. Ask for Phone, email, preferred date, and time. Confirm details before booking. Time zone: Eastern.

Today's date is {{ $now }}

1. Use the get_availability tool to find when I am available. it will return comma separated timeslots the interviewer can meet. check the proposed time against the results. Times are in 24 hour clock times in this format.  2025-03-31T09:00:00-04:00
3. If I am not available, look at get_availability tool again and propose a similar time where I am available
2. use the check_days tool if the user mentions something like next tuesday so you know what date they are talking about
3. Once a time is aggreed upon, output json in this format 
2025-03-28T13:00:00-04:00. 
4. once you have the email, phone start and end time, output only the json and nothing else

{
  \"interview\": {
    \"email\": \"applicant@example.com\",
    \"phone\": \"814-882-1293\",
    \"start_datetime\": \"2025-03-28T10:00:00\",
    \"end_datetime\": \"2025-03-28T11:00:00\"
  }
}

## Rules
- If the calendar is not available at the time requested, do not double book. Send a new time.
- Interviews are all 30 minutes long
- Do not book over another meeting
- do not give details about what is on the interviewers calendar
- do not converse with the user about anything else",
          "returnIntermediateSteps": true
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "ba0fb82e-a280-4392-833e-04f00a47170c",
      "name": "If Final Output",
      "type": "n8n-nodes-base.if",
      "position": [
        960,
        1160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e75b6a50-680f-4f5b-8dd3-fc93be1bc7f1",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.output }}",
              "rightValue": "start_datetime"
            },
            {
              "id": "cadd4bff-8d53-446c-8ad0-14b3fb9ab335",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json.output }}",
              "rightValue": "end_datetime"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "c56bcba9-ac39-474b-a186-ceb67fa4008d",
      "name": "Respond for More Info",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1040,
        1400
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "efd03308-0da1-4797-b899-3d4446eba722",
      "name": "Parse to JSON",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        1400,
        1500
      ],
      "parameters": {
        "jsonSchemaExample": "{
  \"interview\": {
    \"email\": \"applicant@example.com\",
    \"phone\": \"814-882-1293\",
    \"start_datetime\": \"2025-03-28T10:00:00\",
    \"end_datetime\": \"2025-03-28T11:00:00\"
  }
}"
      },
      "typeVersion": 1.2
    },
    {
      "id": "11abd142-d509-4459-bdf5-861dcf4263bf",
      "name": "Set Meeting with Google",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        1640,
        1280
      ],
      "parameters": {
        "end": "={{ $json.output.interview.end_datetime }}",
        "start": "={{ $json.output.interview.start_datetime }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "rbreen.ynteractive@gmail.com",
          "cachedResultName": "rbreen.ynteractive@gmail.com"
        },
        "additionalFields": {
          "summary": "Interview",
          "attendees": [
            "={{ $json.output.interview.email }}"
          ],
          "description": "=I will call you at  {{ $json.output.interview.phone }}"
        }
      },
      "credentials": {
        "googleCalendarOAuth2Api": {
          "id": "nc5M45R7LyFadByw",
          "name": "Google Calendar account"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "fef5ba53-4386-4e88-9f28-8a9b5d9c928f",
      "name": "Final Response to User",
      "type": "n8n-nodes-base.code",
      "position": [
        1640,
        1500
      ],
      "parameters": {
        "jsCode": "const email = $('Convert Output to JSON').first().json.output.interview.email;
const phone = $('Convert Output to JSON').first().json.output.interview.phone;
const start_datetime = $('Convert Output to JSON').first().json.output.interview.start_datetime;
const end_datetime = $('Convert Output to JSON').first().json.output.interview.end_datetime;

let text = `✅ Interview Confirmed!\n\n📧 Email: ${email}\n📞 Phone: ${phone}\n🕒 Start: ${start_datetime}\n🕕 End: ${end_datetime}`;

return { text };
"
      },
      "typeVersion": 2
    },
    {
      "id": "a06664e2-d5d2-40a7-98a5-a3de2d775b7c",
      "name": "Generate Interview Times",
      "type": "n8n-nodes-base.code",
      "position": [
        1620,
        1920
      ],
      "parameters": {
        "jsCode": "function getWeekdaysNextTwoWeeks() {
  const items = [];
  const longDayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

  const today = new Date();
  const endDate = new Date();
  endDate.setDate(today.getDate() + 14); // 2 weeks ahead

  const current = new Date(today);

  while (current <= endDate) {
    const dayOfWeek = current.getDay(); // 0 = Sunday, 6 = Saturday

    // Only weekdays (Mon–Fri)
    if (dayOfWeek >= 1 && dayOfWeek <= 5) {
      const dateStr = current.toISOString().split('T')[0]; // YYYY-MM-DD
      const output = `${longDayNames[dayOfWeek]} - ${dateStr}`;

      items.push({
        json: {
          day: output
        }
      });
    }

    current.setDate(current.getDate() + 1); // Go to next day
  }

  return items;
}

// Example usage:
return getWeekdaysNextTwoWeeks();
"
      },
      "typeVersion": 2
    },
    {
      "id": "f35d595e-6834-4898-bbcb-b17599d769b4",
      "name": "Check My Calendar",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        420,
        1820
      ],
      "parameters": {
        "options": {
          "fields": ""
        },
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "rbreen.ynteractive@gmail.com",
          "cachedResultName": "rbreen.ynteractive@gmail.com"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "googleCalendarOAuth2Api": {
          "id": "nc5M45R7LyFadByw",
          "name": "Google Calendar account"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "29e3a097-b6f1-4a54-b943-d9ad9177b03b",
      "name": "Split Events into 30 min blocks",
      "type": "n8n-nodes-base.code",
      "position": [
        620,
        1820
      ],
      "parameters": {
        "jsCode": "const events = items.map(item => item.json);
const intervalMinutes = 30;
const timeZone = 'America/New_York';

function formatToEastern(date) {
  const tzDate = new Intl.DateTimeFormat('en-US', {
    timeZone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false
  }).formatToParts(date).reduce((acc, part) => {
    if (part.type !== 'literal') acc[part.type] = part.value;
    return acc;
  }, {});

  const offset = getEasternOffset(date);
  return `${tzDate.year}-${tzDate.month}-${tzDate.day}T${tzDate.hour}:${tzDate.minute}:${tzDate.second}${offset}`;
}

function getEasternOffset(date) {
  const options = { timeZone, timeZoneName: 'short' };
  const parts = new Intl.DateTimeFormat('en-US', options).formatToParts(date);
  const tzName = parts.find(p => p.type === 'timeZoneName').value;
  return tzName.includes('EDT') ? '-04:00' : '-05:00';
}

function alignToPreviousSlot(date) {
  const aligned = new Date(date);
  const minutes = aligned.getMinutes();
  aligned.setMinutes(minutes < 30 ? 0 : 30, 0, 0);
  return aligned;
}

function alignToNextSlot(date) {
  const aligned = new Date(date);
  const minutes = aligned.getMinutes();
  if (minutes > 0 && minutes <= 30) {
    aligned.setMinutes(30, 0, 0);
  } else if (minutes > 30) {
    aligned.setHours(aligned.getHours() + 1);
    aligned.setMinutes(0, 0, 0);
  } else {
    aligned.setMinutes(0, 0, 0);
  }
  return aligned;
}

const splitEventIntoETBlocks = (event) => {
  const blocks = [];

  let current = alignToPreviousSlot(new Date(event.start.dateTime));
  const eventEnd = alignToNextSlot(new Date(event.end.dateTime));

  while (current < eventEnd) {
    const blockEnd = new Date(current);
    blockEnd.setMinutes(current.getMinutes() + intervalMinutes);

    blocks.push({
      start: formatToEastern(current),
      end: formatToEastern(blockEnd)
    });

    current = blockEnd;
  }

  return blocks;
};

let allBlocks = [];
for (const event of events) {
  if (event.start?.dateTime && event.end?.dateTime) {
    const blocks = splitEventIntoETBlocks(event);
    allBlocks = allBlocks.concat(blocks);
  }
}

return allBlocks.map(block => ({ json: block }));
"
      },
      "typeVersion": 2
    },
    {
      "id": "f9297e8a-75dd-4f12-b0e1-d3fa372a7631",
      "name": "Add Blocked Field",
      "type": "n8n-nodes-base.set",
      "position": [
        800,
        1840
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f1270be8-1d11-4086-8bc0-ae53c99507c1",
              "name": "start",
              "type": "string",
              "value": "={{ $json.start }}"
            },
            {
              "id": "1a5f24ff-7d0c-436d-bb0b-015fc0c85cb7",
              "name": "end",
              "type": "string",
              "value": "={{ $json.end }}"
            },
            {
              "id": "befe6645-c0c1-40eb-9ba6-eccf2a762247",
              "name": "Blocked",
              "type": "string",
              "value": "Blocked"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "8ba70f94-e9e6-44aa-b0e7-9a5294634e0e",
      "name": "Generate 30 Minute Timeslots",
      "type": "n8n-nodes-base.code",
      "position": [
        440,
        2020
      ],
      "parameters": {
        "jsCode": "const slots = [];
const slotMinutes = 30;
const timeZone = 'America/New_York';
const businessStartHour = 9;
const businessEndHour = 17;

// Get offset like -04:00 or -05:00
function getEasternOffset(date) {
  const options = { timeZone, timeZoneName: 'short' };
  const parts = new Intl.DateTimeFormat('en-US', options).formatToParts(date);
  const tz = parts.find(p => p.type === 'timeZoneName')?.value || 'EST';
  return tz.includes('EDT') ? '-04:00' : '-05:00';
}

// Format Date as ISO with Eastern offset
function formatToEasternISO(date) {
  const formatter = new Intl.DateTimeFormat('en-CA', {
    timeZone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    hour12: false,
  });

  const parts = formatter.formatToParts(date).reduce((acc, part) => {
    if (part.type !== 'literal') acc[part.type] = part.value;
    return acc;
  }, {});

  const offset = getEasternOffset(date);
  return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}${offset}`;
}

// Convert a Date to the hour/minute of its Eastern time
function getEasternTimeParts(date) {
  const formatter = new Intl.DateTimeFormat('en-US', {
    timeZone,
    hour: '2-digit',
    minute: '2-digit',
    hour12: false,
  });
  const [hourStr, minStr] = formatter.format(date).split(':');
  return { hour: parseInt(hourStr), minute: parseInt(minStr) };
}

const now = new Date();
const endDate = new Date(now);
endDate.setDate(now.getDate() + 7);

// Set current time to 24 hours in the future
const current = new Date(now);
current.setHours(current.getHours() + 24);

// Round to the next 30-minute block in Eastern time
const { minute } = getEasternTimeParts(current);
if (minute < 30) {
  current.setMinutes(30, 0, 0);
} else {
  current.setHours(current.getHours() + 1);
  current.setMinutes(0, 0, 0);
}

// Generate 30-minute blocks only during business hours & weekdays
while (current < endDate) {
  const dayOfWeek = current.getDay(); // 0 = Sunday, 6 = Saturday

  // Skip weekends
  if (dayOfWeek !== 0 && dayOfWeek !== 6) {
    const { hour } = getEasternTimeParts(current);

    if (hour >= businessStartHour && hour < businessEndHour) {
      const start = new Date(current);
      const end = new Date(start);
      end.setMinutes(start.getMinutes() + slotMinutes);

      slots.push({
        start: formatToEasternISO(start),
        end: formatToEasternISO(end),
      });
    }
  }

  current.setMinutes(current.getMinutes() + slotMinutes);
}

return slots.map(slot => ({ json: slot }));
"
      },
      "typeVersion": 2
    },
    {
      "id": "3ea13a0a-d496-40b8-9321-6bc3df415191",
      "name": "Combine My Calendar with All Slots",
      "type": "n8n-nodes-base.merge",
      "position": [
        780,
        2020
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "joinMode": "enrichInput2",
        "fieldsToMatchString": "start, end"
      },
      "typeVersion": 3
    },
    {
      "id": "ad57e0b4-43d0-4991-adc3-e325e2405e93",
      "name": "Check if Calendar Blocked",
      "type": "n8n-nodes-base.if",
      "position": [
        1100,
        1820
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "af65c6c8-31c7-4f27-a073-cf7f72079882",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.Blocked }}",
              "rightValue": "Blocked"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "6e427266-1f64-4492-b4c0-30d03d6a20de",
      "name": "Return string of all available times",
      "type": "n8n-nodes-base.code",
      "position": [
        1160,
        2000
      ],
      "parameters": {
        "jsCode": "const formatted = items.map(item => {
  const start = item.json.start;
  const end = item.json.end;
  return `${start} - ${end}`;
});

const combined = formatted.join(', ');

return [
  {
    json: {
      availableSlots: combined
    }
  }
];
"
      },
      "typeVersion": 2
    },
    {
      "id": "3f26c921-2d4c-4e8a-a551-801c2a94086a",
      "name": "Get Availability",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        220,
        1920
      ],
      "parameters": {
        "inputSource": "passthrough"
      },
      "typeVersion": 1.1
    },
    {
      "id": "6d34f9e2-4c43-4e0b-a54d-2c8076ee6fe0",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -420,
        1160
      ],
      "parameters": {
        "color": 5,
        "width": 520,
        "height": 1000,
        "content": "How to Use the Interview Scheduler Workflow in n8n
________________________________________
✨ Overview
This workflow allows candidates to schedule interviews by chatting with an AI assistant. It checks your Google Calendar availability, identifies free 30-minute weekday slots between 9am-5pm EST, and automatically books a meeting once details are confirmed.
________________________________________
⚡ Prerequisites
1.	OpenAI Account
o	API Key with GPT-4o model access
2.	Google Account with Calendar Access
o	Your calendar must be accessible via Google Calendar
3.	OAuth2 Credentials for Google Calendar API configured in n8n
4.	OpenAI Credentials configured in n8n
________________________________________
🔐 API Credentials Setup
Google Calendar OAuth2:
•	Create a project called n8n in google cloud console
•	Go to n8n > Credentials
•	Create new Google Calendar OAuth2 API credentials
•	Authorize your Google account (e.g., yourname@gmail.com)
OpenAI:
•	Go to Credentials
•	Create new OpenAI API credentials
•	Enter your OpenAI API key and give it a label (e.g., \"My OpenAI Key\")
________________________________________
🔧 How to Make It Yours
✅ Update These Workflow Fields:
1.	Google Calendar Email
o	Replace all instances of rbreen.ynteractive@gmail.com with your own Google Calendar email.
o	This appears in:
	Google Calendar Nodes
	ToolWorkflow JSON for \"Run Get Availability\"
2.	Google Calendar OAuth2 Credential Name
o	Replace credential name Google Calendar account with your own credential name.
3.	OpenAI Credential Name
o	Replace OpenAi account with your own OpenAI credential name.
4.	Webhook URL / Chat Interface
o	Go to the Candidate Chat node
o	Copy the webhook URL
o	Share this public link with users to start the chatbot
5.	System Message Instructions (Optional)
o	You can tweak the system message in the Interview Scheduler agent node to change tone, questions, or rules.
6.	Custom Branding (Optional)
o	Update the title and subtitle in the Candidate Chat node under options
o	You can also replace the final message in Final Response to User with your own branding/tone
________________________________________


"
      },
      "typeVersion": 1
    },
    {
      "id": "07ef21ee-c02a-4145-a0fb-3ecc260ff585",
      "name": "When chat message received",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "position": [
        280,
        1220
      ],
      "webhookId": "0c8f9f17-f5f3-4b5d-85e7-071ced0213ae",
      "parameters": {
        "public": true,
        "options": {}
      },
      "typeVersion": 1.1
    }
  ],
  "active": true,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "69e8aa1b-e404-44ed-aedc-7d8480e2383e",
  "connections": {
    "Parse to JSON": {
      "ai_outputParser": [
        [
          {
            "node": "Convert Output to JSON",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "If Final Output": {
      "main": [
        [
          {
            "node": "Convert Output to JSON",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond for More Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "check day names": {
      "ai_tool": [
        [
          {
            "node": "Interview Scheduler",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Get Availability": {
      "main": [
        [
          {
            "node": "Check My Calendar",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate 30 Minute Timeslots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Blocked Field": {
      "main": [
        [
          {
            "node": "Combine My Calendar with All Slots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check My Calendar": {
      "main": [
        [
          {
            "node": "Split Events into 30 min blocks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model2": {
      "ai_languageModel": [
        [
          {
            "node": "Interview Scheduler",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model4": {
      "ai_languageModel": [
        [
          {
            "node": "Convert Output to JSON",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Interview Scheduler": {
      "main": [
        [
          {
            "node": "If Final Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Get Availability": {
      "ai_tool": [
        [
          {
            "node": "Interview Scheduler",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Respond for More Info": {
      "main": [
        []
      ]
    },
    "Window Buffer Memory2": {
      "ai_memory": [
        [
          {
            "node": "Interview Scheduler",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Convert Output to JSON": {
      "main": [
        [
          {
            "node": "Set Meeting with Google",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Final Response to User": {
      "main": [
        []
      ]
    },
    "Set Meeting with Google": {
      "main": [
        [
          {
            "node": "Final Response to User",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if Calendar Blocked": {
      "main": [
        [
          {
            "node": "Return string of all available times",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "Interview Scheduler",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate 30 Minute Timeslots": {
      "main": [
        [
          {
            "node": "Combine My Calendar with All Slots",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Split Events into 30 min blocks": {
      "main": [
        [
          {
            "node": "Add Blocked Field",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine My Calendar with All Slots": {
      "main": [
        [
          {
            "node": "Check if Calendar Blocked",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

功能特点

  • 自动检测新邮件
  • AI智能内容分析
  • 自定义分类规则
  • 批量处理能力
  • 详细的处理日志

技术分析

节点类型及作用

  • @N8N/N8N Nodes Langchain.Lmchatopenai
  • @N8N/N8N Nodes Langchain.Memorybufferwindow
  • @N8N/N8N Nodes Langchain.Toolworkflow
  • Stickynote
  • @N8N/N8N Nodes Langchain.Agent

复杂度评估

配置难度:
★★★★☆
维护难度:
★★☆☆☆
扩展性:
★★★★☆

实施指南

前置条件

  • 有效的Gmail账户
  • n8n平台访问权限
  • Google API凭证
  • AI分类服务订阅

配置步骤

  1. 在n8n中导入工作流JSON文件
  2. 配置Gmail节点的认证信息
  3. 设置AI分类器的API密钥
  4. 自定义分类规则和标签映射
  5. 测试工作流执行
  6. 配置定时触发器(可选)

关键参数

参数名称 默认值 说明
maxEmails 50 单次处理的最大邮件数量
confidenceThreshold 0.8 分类置信度阈值
autoLabel true 是否自动添加标签

最佳实践

优化建议

  • 定期更新AI分类模型以提高准确性
  • 根据邮件量调整处理批次大小
  • 设置合理的分类置信度阈值
  • 定期清理过期的分类规则

安全注意事项

  • 妥善保管API密钥和认证信息
  • 限制工作流的访问权限
  • 定期审查处理日志
  • 启用双因素认证保护Gmail账户

性能优化

  • 使用增量处理减少重复工作
  • 缓存频繁访问的数据
  • 并行处理多个邮件分类任务
  • 监控系统资源使用情况

故障排除

常见问题

邮件未被正确分类

检查AI分类器的置信度阈值设置,适当降低阈值或更新训练数据。

Gmail认证失败

确认Google API凭证有效且具有正确的权限范围,重新进行OAuth授权。

调试技巧

  • 启用详细日志记录查看每个步骤的执行情况
  • 使用测试邮件验证分类逻辑
  • 检查网络连接和API服务状态
  • 逐步执行工作流定位问题节点

错误处理

工作流包含以下错误处理机制:

  • 网络超时自动重试(最多3次)
  • API错误记录和告警
  • 处理失败邮件的隔离机制
  • 异常情况下的回滚操作